package edu.drexel.cs544.group11.client.protocol;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.math.BigInteger;
import java.net.ConnectException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.net.Socket;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;

import edu.drexel.cs544.group11.common.MessageFormatException;
import edu.drexel.cs544.group11.common.RequestCommand;
import edu.drexel.cs544.group11.common.RequestMessage;
import edu.drexel.cs544.group11.common.ResponseMessage;
import edu.drexel.cs544.group11.common.ResponseStatusCode;

/**
 * SERVICE
 * 
 * Understands how to connect to the server.
 * 
 * CS 544
 * 
 * @author Group 11:
 * @author Jason Setzer
 * @author Tam Thongsima
 * @author Kenny Oyefara
 * @author Chase Miller
 * 
 * @date Spring 2013
 * 
 */
public class ServerConnection {
    private int autoport = 5944; // SERVICE
    private int port = 5943; // SERVICE
    
    private PrintStream outToServer;
    private BufferedReader inFromServer;
    private Socket server_connect;
    
    private int seqId;
    private String serverAddress;
    private String keyStorePath;
    
    private static double version = 1.0;
    
    /**
     * Creates a ServerConnection using the autodiscover feature.
     */
    protected ServerConnection(String keyStorePath) {
	this.seqId = 0;
	this.serverAddress = null;
	this.keyStorePath = keyStorePath;
    }
    
    /**
     * Creates an object using the specified server address.
     * 
     * @param serverAddress
     */
    protected ServerConnection(String keyStorePath, String serverAddress) {
	this.seqId = 0;
	this.serverAddress = serverAddress;
	this.keyStorePath = keyStorePath;
    }
    
    /**
     * Answers connecting to the BSP server.
     * 
     * @param ip - the IP address of the server to connect to
     * 
     * @param port
     * @throws Exception
     */
    public void establishConnection() throws ConnectException {
	try {
	    System.out.println("Connecting to server...");
	    
	    if (this.serverAddress == null) {
		this.serverAddress = this.getServerAddressViaAutoDiscover();
		
		if (this.serverAddress == null) {
		    System.out.println("Could not auto discover server.  Please run again with the server address specified as an argument.");
		    throw new ConnectException("Could not connect to server");
		}
	    }
	    
	    System.setProperty("javax.net.ssl.trustStore", this.keyStorePath); //Client just trusts server certificate
	    SSLSocketFactory ssf = (SSLSocketFactory) SSLSocketFactory.getDefault();
	    this.server_connect = ssf.createSocket(this.serverAddress, port);
	    
	    SSLSession session = ((SSLSocket) server_connect).getSession();
	    Certificate[] cchain = session.getPeerCertificates();
	    
	    System.out.println("\nConnection established to server address " + this.serverAddress);
	    System.out.println("\nDetails:");
	    
	    for (int i = 0; i < cchain.length; i++) {
		System.out.println(((X509Certificate) cchain[i]).getSubjectDN());
	    }
	    System.out.println("Peer host is " + session.getPeerHost());
	    System.out.println("Cipher is " + session.getCipherSuite());
	    System.out.println("Protocol is " + session.getProtocol());
	    System.out.println("ID is " + new BigInteger(session.getId()));
	    System.out.println("Session created in " + session.getCreationTime());
	    System.out.println("Session accessed in " + session.getLastAccessedTime());
	    
	    this.inFromServer = new BufferedReader(new InputStreamReader(server_connect.getInputStream()));//Create object to read data to ssl socket
	    this.outToServer = new PrintStream(server_connect.getOutputStream());//Create object to write data to ssl socket
	    
	    ResponseMessage serverConnectionResponse = this.waitForResponse();
	    
	    if (serverConnectionResponse.getStatusCode() != ResponseStatusCode.SUCCESS) {
		throw new ConnectException("Server returned an error while trying to connect");
	    }
	}
	catch (Exception e) {
	    throw new ConnectException("Could not connect to server");
	}
    }
    
    /**
     * Waits for a response and returns a ResponseMessage object.
     * 
     * @return
     */
    public ResponseMessage waitForResponse() throws IOException {
	ResponseMessage response = null;
	String fullMessage = "";
	
	while (!fullMessage.endsWith(ResponseMessage.getEndOfMessage())) {
	    String nextLine = this.inFromServer.readLine();
	    nextLine += "\n";
	    fullMessage += nextLine;
	}
	
	try {
	    response = new ResponseMessage(fullMessage);
	}
	catch (MessageFormatException e) {
	    throw new IOException("Response from server was malformed", e);
	}
	
	this.seqId = response.getSequenceId();
	
	return response;
    }
    
    /**
     * Auto discover the server.
     * 
     * @return
     */
    @SuppressWarnings("null")
    private String getServerAddressViaAutoDiscover() {
	System.out.println("Auto-discovering server...will timeout in 10 seconds...");
	int x = 0;
	DatagramPacket inPacket = null;
	byte[] inBuf = new byte[256];
	String fromserver = "";
	String ipaddress = null;
	try {
	    MulticastSocket socket1 = new MulticastSocket(autoport);
	    socket1.setSoTimeout(10000);
	    InetAddress group = InetAddress.getByName("225.3.3.4");
	    socket1.joinGroup(group);
	    
	    inPacket = new DatagramPacket(inBuf, inBuf.length);
	    socket1.receive(inPacket);
	    fromserver = new String(inPacket.getData());
	    
	    if (fromserver.contains("BSP:")) {
		return parseServerAddress(fromserver);
	    }
	    else {
		return null;
	    }
	    
	}
	catch (IOException ioe) {
	    ipaddress = null;
	}
	
	return ipaddress;
    }
    
    /**
     * Parses an ip from the provided server.
     * 
     * @param fromserver
     * @return - returns null if parsing fails.
     */
    private static String parseServerAddress(String fromserver) {
	String IP = ("(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})");
	String ipaddress = "";
	Pattern pattern = Pattern.compile(IP);
	Matcher match = pattern.matcher(fromserver);
	while (match.find()) {
	    ipaddress = match.group();
	}
	return ipaddress;
    }
    
    public PrintStream getOutToServer() {
	return outToServer;
    }
    
    public BufferedReader getInFromServer() {
	return inFromServer;
    }
    
    /**
     * Returns this connection's current seq id.
     * 
     * @return
     */
    public int getSeqId() {
	return this.seqId;
    }
    
    /**
     * Answers closing the connection.
     * 
     * @throws IOException
     */
    public void close() throws IOException {
	if (this.server_connect != null && !this.server_connect.isClosed()) {
	    System.out.println("Disconnecting from server...");
	    RequestMessage close = new RequestMessage(RequestCommand.END_SESSION, this.seqId, version);
	    
	    try {
		close.sendMessage(this.outToServer);
	    }
	    catch (MessageFormatException e) {
	    }
	    
	    this.server_connect.close();
	}
    }
    
    /**
     * Sends the provided request
     * 
     * @param request
     * @throws IOException
     * @throws MessageFormatException
     */
    public void sendRequest(RequestMessage request) throws IOException {
	try {
	    request.sendMessage(this.outToServer);
	}
	catch (MessageFormatException e) {
	    e.printStackTrace();
	}
    }
}
